[Rust] rustlsとOSの証明書機能を使ったTLS接続

[Rust] rustlsとOSの証明書機能を使ったTLS接続

Clock Icon2024.11.08

Introduction

rustlsはRustで書かれたTLSライブラリです。
OpenSSLおよびBoringSSLのパフォーマンスを超えたとも言われています。

また、rustls-platform-verifierは、TLS証明書の検証をOSの証明書機能に基づいて証明書を検証するライブラリです。
※Macであればキーチェーンを使う

今回はこれらを使ってクライアントとサーバプログラムを作成し、TLS通信してみます。

Environment

  • MacBook Pro (14-inch, M3, 2023)
  • OS : MacOS 14.5
  • Rust : 1.81.0

Try

ではCargoでプロジェクトを作成し、serverプログラムとclientプログラムでTLS通信してみます。
まずはCargoでプロジェクトを作成します。

% cargo new rusttls-example && cd rusttls-example

Cargo.tomlを下記のように記述。

[dependencies]
env_logger = "0.11.5"
hex = "0.4.3"
log = "0.4.22"
rustls = "0.23.15"
rustls-pemfile = "2.2.0"
rustls-platform-verifier = "0.3.4"
tokio = { version = "1", features = ["full"] }
tokio-rustls = "0.26.0"

[[example]]
name = "tls-server"
path = "src/tls-server.rs"

[[example]]
name = "tls-client"
path = "src/tls-client.rs"

ターミナルで自己署名証明書の生成します。
とりあえずテスト用なので適当に作成します。

% openssl req -x509 -newkey rsa:4096 -nodes -keyout server.key -out server.crt -days 365 -subj "/CN=localhost"

Server

TLSサーバ側の主な処理です。
証明書(公開鍵を含む)と秘密鍵をロードします。

let cert_file = std::fs::read("server.crt")?;
let key_file = std::fs::read("server.key")?;

// 証明書の読み込み
let certs = rustls_pemfile::certs(&mut &*cert_file)
    .collect::<Result<Vec<_>, _>>()?
    .into_iter()
    .map(CertificateDer::from)
    .collect();

TLSサーバーの設定。

let mut config = ServerConfig::builder()
    .with_no_client_auth()
    .with_single_cert(certs, key)?;

// ALPNプロトコルの設定(今回はHTTPを使わないのでなくてもOK)
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];

あとはリスナーを作成します。
8443番ポートで待ち受けてサーバ側の処理はOKです。
↓がtls-server.rs全文です。

use rustls::{
    pki_types::{CertificateDer, PrivateKeyDer},
    ServerConfig,
};
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use rustls::crypto::ring::default_provider;

async fn run_tls_server() -> Result<(), Box<dyn std::error::Error>> {
    // CryptoProvider
    default_provider().install_default();

    let cert_file = std::fs::read("server.crt")?;
    let key_file = std::fs::read("server.key")?;

    println!("Loading certificate and key...");

    let certs = rustls_pemfile::certs(&mut &*cert_file)
        .collect::<Result<Vec<_>, _>>()?
        .into_iter()
        .map(CertificateDer::from)
        .collect();

    println!("Certificate loaded successfully");

    let key = {
        let mut reader = &mut &*key_file;
        let mut private_keys = Vec::new();

        for item in rustls_pemfile::read_all(&mut reader) {
            match item {
                Ok(rustls_pemfile::Item::Pkcs1Key(key)) => {
                    println!("Found PKCS1 key");
                    private_keys.push(PrivateKeyDer::Pkcs1(key));
                }
                Ok(rustls_pemfile::Item::Pkcs8Key(key)) => {
                    println!("Found PKCS8 key");
                    private_keys.push(PrivateKeyDer::Pkcs8(key));
                }
                Ok(_) => println!("Found other item"),
                Err(e) => println!("Error reading key: {}", e),
            }
        }

        private_keys
            .into_iter()
            .next()
            .ok_or("no private key found")?
    };

    println!("Private key loaded successfully");

    let mut config = ServerConfig::builder()
        .with_no_client_auth()
        .with_single_cert(certs, key)?;

    config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];

    println!("Server configuration created successfully");

    let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config));
    let listener = TcpListener::bind("127.0.0.1:8443").await?;
    println!("TLS Server listening on port 8443");

    while let Ok((stream, addr)) = listener.accept().await {
        println!("Accepted connection from: {}", addr);
        let acceptor = acceptor.clone();

        tokio::spawn(async move {
            match acceptor.accept(stream).await {
                Ok(mut tls_stream) => {
                    println!("TLS connection established with: {}", addr);
                    let mut buf = [0; 1024];
                    match tls_stream.read(&mut buf).await {
                        Ok(n) => {
                            println!("\n=== TLS Server Received ===");
                            println!("Decrypted text: {}", String::from_utf8_lossy(&buf[..n]));
                            println!("Raw bytes: {}", hex::encode(&buf[..n]));

                            if let Err(e) = tls_stream.write_all(&buf[..n]).await {
                                eprintln!("Failed to write to TLS socket: {}", e);
                            }
                        }
                        Err(e) => eprintln!("Failed to read from TLS socket: {}", e),
                    }
                }
                Err(e) => eprintln!("TLS acceptance failed: {}", e),
            }
        });
    }
    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    run_tls_server().await
}

サーバを起動し、クライアントから正しく接続できれば
復号したテキストが表示されます。

Client

次はクライアント側のプログラムです。
まずはTLSクライアントの設定をします。

// プラットフォーム(macなのでkey chain)の証明書検証を設定
let _verifier = Verifier::new();
let mut config = rustls_platform_verifier::tls_config();

// ALPNプロトコルの設定(なくてもOK)
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];

そしてTLSサーバへ接続とデータ送受信を実施します。

let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
let server_name = rustls::pki_types::ServerName::try_from("localhost")?
    .to_owned();

// TCP接続とTLSハンドシェイク
let stream = TcpStream::connect("127.0.0.1:8443").await?;
let mut tls_stream = connector.connect(server_name, stream).await?;

// データ送信
tls_stream.write_all(message).await?;

// データ受信
let mut buf = [0; 1024];
let n = tls_stream.read(&mut buf).await?;

クライアント側プログラムの全文です。

use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use rustls::crypto::ring::default_provider;
use std::sync::Arc;
use rustls_platform_verifier::Verifier;
use std::env;

async fn run_tls_client() -> Result<(), Box<dyn std::error::Error>> {
    env::set_var("RUST_LOG", "debug,rustls=debug");
    env_logger::init();

    // CryptoProviderをインストール
    if let Err(e) = default_provider().install_default() {
        eprintln!("Failed to install default provider: {:?}", e);
    }
    println!("Creating TLS client configuration...");

    // プラットフォームの証明書検証を設定
    let _verifier = Verifier::new();
    println!("Platform verifier created");

    // TLS設定を作成
    let mut config = rustls_platform_verifier::tls_config();

    // ALPNプロトコルの設定(なくてもOK)
    config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];

    println!("Connecting to server...");

    // TLS接続の準備
    let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
    let server_name = rustls::pki_types::ServerName::try_from("localhost")?
        .to_owned();

    // TCP接続
    let stream = TcpStream::connect("127.0.0.1:8443").await?;
    println!("TCP connection established");

    // TLSハンドシェイク
    let mut tls_stream = connector.connect(server_name, stream).await
        .map_err(|e| {
            eprintln!("TLS connection failed: {}", e);
            e
        })?;
    println!("TLS connection established");

    // テストメッセージの送信
    let message = b"Hello, World!";

    println!("\n=== TLS Client Sending ===");
    println!("Sending text: {}", String::from_utf8_lossy(message));
    println!("Raw bytes: {}", hex::encode(message));

    tls_stream.write_all(message).await?;

    // サーバーからの応答を受信
    let mut buf = [0; 1024];
    let n = tls_stream.read(&mut buf).await?;

    println!("\n=== TLS Client Received ===");
    println!("Decrypted text: {}", String::from_utf8_lossy(&buf[..n]));
    println!("Raw bytes: {}", hex::encode(&buf[..n]));

    // TLS接続を正しく終了
    tls_stream.shutdown().await?;
    println!("Connection closed successfully");

    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    run_tls_client().await
}

動作確認

では動かしてみます。
このプログラム(tls-server.rsとtls-client.rs)を実行すると、
OSの証明書ストア(今回はキーチェーン)を使用して証明書の検証が行われ、
TLS通信が確立され、暗号化されたデータの送受信が行われます。

まずはTLSサーバを起動。

% cargo run --example tls-server

・・・

Loading certificate and key...
Certificate loaded successfully
Found PKCS8 key
Private key loaded successfully
Server configuration created successfully
TLS Server listening on port 8443

クライアントを実行してみます。
最初に証明書をインストールせずに実行してみます。
証明書がないので通信に失敗します。

% cargo run --example tls-client
Creating TLS client configuration...
Platform verifier created
Connecting to server...
TCP connection established
・・・
[2024-11-06T01:39:25Z ERROR rustls_platform_verifier::verification::apple] failed to verify TLS certificate: invalid peer certificate: Other(OtherError("“localhost” certificate is not trusted: -67843"))
TLS connection failed: invalid peer certificate: Other(OtherError("“localhost” certificate is not trusted: -67843"))
Error: Custom { kind: InvalidData, error: InvalidCertificate(Other(OtherError("“localhost” certificate is not trusted: -67843"))) }

証明書をkey chainに追加してから再度実行してみます。

% sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain server.crt

キーチェーンから証明書を検索してちゃんとインストールされているか確認してみます。
下記のように表示されれば、証明書が信頼された状態となりTLS接続が成功します。
※テスト用です

% security find-certificate -a -c "localhost"
keychain: "/Library/Keychains/System.keychain"
version: 256
class: 0x80001000
attributes:
    "alis"<blob>="localhost"
    "cenc"<uint32>=0x00000003
    "ctyp"<uint32>=0x00000001
    "hpky"<blob>=0x8FDEE7AAE1F2FD9AE5BEF46DFF2D95546A17ED37  "\217\336\347\252\341\362\375\232\345\276\364m\377-\225Tj\027\3557"
    "issu"<blob>=0x30143112301006035504030C096C6F63616C686F7374  "0\0241\0220\020\006\003U\004\003\014\011localhost"
    "labl"<blob>="localhost"
    "skid"<blob>=0x8FDEE7AAE1F2FD9AE5BEF46DFF2D95546A17ED37  "\217\336\347\252\341\362\375\232\345\276\364m\377-\225Tj\027\3557"
    "snbr"<blob>=0x1796BADAB78E8B5A8FE92786680B901327D109A5  "\027\226\272\332\267\216\213Z\217\351'\206h\013\220\023'\321\011\245"
    "subj"<blob>=0x30143112301006035504030C096C6F63616C686F7374  "0\0241\0220\020\006\003U\004\003\014\011localhost"

今度はクライアント接続も成功しました。

% cargo run --example tls-client
Creating TLS client configuration...
Platform verifier created
Connecting to server...
TCP connection established
[2024-11-06T01:41:10Z DEBUG rustls::client::hs] No cached session for DnsName("localhost")
[2024-11-06T01:41:10Z DEBUG rustls::client::hs] Not resuming any session
[2024-11-06T01:41:10Z DEBUG rustls::client::hs] Using ciphersuite TLS13_AES_256_GCM_SHA384
[2024-11-06T01:41:10Z DEBUG rustls::client::tls13] Not resuming
[2024-11-06T01:41:10Z DEBUG rustls::client::tls13] TLS1.3 encrypted extensions: [Protocols([ProtocolName(6832)]), ServerNameAck]
[2024-11-06T01:41:10Z DEBUG rustls::client::hs] ALPN protocol is Some(b"h2")
TLS connection established

=== TLS Client Sending ===
Sending text: Hello, World!
Raw bytes: 48656c6c6f2c20576f726c6421

=== TLS Client Received ===
Decrypted text: Hello, World!
Raw bytes: 48656c6c6f2c20576f726c6421
[2024-11-06T01:41:10Z DEBUG rustls::common_state] Sending warning alert CloseNotify
Connection closed successfully

サーバ側にもログがでてます。

=== TLS Server Received ===
Decrypted text: Hello, World!
Raw bytes: 48656c6c6f2c20576f726c6421

Summary

今回はrusttlsを使ってクライアント-サーバ間でTLS接続を実装してみました。
なんか昔Delphiとcgiのクラサバでこんな感じで証明書インストールして使ってた気がします。

References

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.